CSS Friendly Adapters (Menu) for Artisteer templates


I have been working on a site called Crazy Cancer Tour (more details later). To quickly and easily get a look, I used a product called Artisteer (www.artisteer.com). I am not sure I will stick with Artisteer, but it does provide a quick and easy way of creating a site.

Unfortunately, Artisteer is also a bit class heavy, so I will clean up this site a bit later with a more proper implementation, but this is getting it running for now.

Setting Up Your Site

There are two options here:

  1. Install the VSI and use the CSS Friendly Adapter template (it is linked from www.asp.net)
  2. Retrofit the site

If you plan on using any new features (AJAX, etc.), you will have to go to option #2, or copy a lot of stuff from the site created from the template installed with the VSI. As the VSI route is pretty simple, let’s go the other route. The first few steps are really easy:

  1. Copy the file CSSFriendlyAdapters.browser to the App_Browsers folder of your site
  2. Copy the following files from the JavaScript folder to your site
    1. AdapterUtils.js
    2. MenuAdapter.js
  3. Copy Menu.css from the CSS folder in CSS Friendly to your CSS folder
  4. Create a folder css/BrowserSpecific and copy the IEMenu6.css file here

Testing The Setup

The next step is to create a page to test the setup. Create a new page and drop a menu control on the page. Click on the context menu (the little chevron to the upper right when you hover on the menu) and select new Data Source on the Data Source drop down (topmost dropdown). Choose SiteMapDataSource. The section should look like this:

<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1" CssSelectorClass="art-nav">
</asp:Menu>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />

Add a Web.sitemap file and paste the following into it:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="
http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="default.aspx" title="Home"  description="Home page for site">
        <siteMapNode url="about/default.aspx" title="About"  description="About Us" />
      <siteMapNode url="help/default.aspx" title="Help"  description="FAQs and SiteMap">
        <siteMapNode url="help/faq.aspx" title="FAQ"  description="Frequently Asked Questions" />
        <siteMapNode url="help/sitemap.aspx" title="Sitemap"  description="Map of Site" />
      </siteMapNode>
    </siteMapNode>
</siteMap>

If you preview now, the menu should work, but it would be better to add one more property to the Menu so the About and Help do not cascade under home. You do this by adding the Attribute StaticDisplayLevels and setting it to 2. I also like Horizontal menus, so Orientation should be Horizontal, as shown below. Note that I also set the ShowStartingNode to true on the SiteMapDataSource, which is more explicit, but not necessary:

<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1" CssSelectorClass="art-nav">
StaticDisplayLevels="2" Orientation="Horizontal" />
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server"
ShowStartingNode="true" />

Running it I now see the CSS is working.

Altering the Menu

As mentioned in my previous post on this topic, the default CSS adapters are a bit too class heavy. Here is the current HTML produced by the site:

<div class="art-nav" id="Menu1">
  <div class="AspNet-Menu-Horizontal">
      <ul class="AspNet-Menu">
        <li class="AspNet-Menu-WithChildren">
          <a href="/CrazyCancerTour.UI.Website/default.aspx" class="AspNet-Menu-Link" title="Home page for site">
            Home</a>
          <ul>
            <li class="AspNet-Menu-Leaf">
              <a href="/CrazyCancerTour.UI.Website/about/default.aspx" class="AspNet-Menu-Link" title="About Us">
                About</a>
            </li>
            <li class="AspNet-Menu-WithChildren">
              <a href="/CrazyCancerTour.UI.Website/help/default.aspx" class="AspNet-Menu-Link" title="FAQs and SiteMap">
                Help</a>
              <ul>
                <li class="AspNet-Menu-Leaf">
                  <a href="/CrazyCancerTour.UI.Website/help/faq.aspx" class="AspNet-Menu-Link" title="Frequently Asked Questions">
                    FAQ</a>
                </li>
                <li class="AspNet-Menu-Leaf">
                  <a href="/CrazyCancerTour.UI.Website/help/sitemap.aspx" class="AspNet-Menu-Link" title="Map of Site">
                    Sitemap</a>
                </li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
  </div>
</div>

This is pretty much what I showed in the last post, only shorter. What I want is an option that works with Artisteer, so here goes. First, I need the CSS to look like the following:

<div class="art-nav" id="Menu1">
    <div class="l"></div>
    <div class="r"></div>
    <ul class="art-menu">
        <li>
            <a href="/CrazyCancerTour.UI.Website/default.aspx" class="active">
            <span class="l"></span><span class="r"></span>
            <span class="t">Home</span></a>
        </li>
        <li>
            <a href="/CrazyCancerTour.UI.Website/about/default.aspx">
            <span class="l"></span><span class="r"></span>
            <span class="t">Archive</span></a>
        </li>
        <li>
            <a href="/CrazyCancerTour.UI.Website/help/default.aspx">
            <span class="l"></span><span class="r"></span>
            <span class="t">Help</span></a>
            <ul>
                <li><a href="/CrazyCancerTour.UI.Website/help/faq.aspx">FAQ</a>
                </li>
                <li><a href="/CrazyCancerTour.UI.Website/help/sitemap.aspx">SiteMap</a>
                </li>
            </ul>
        </li>
    </ul>
</div>

Like I said, not my final approach, but it is a start. And, with this simple menu, I have cut out 288 bytes in weight.

Altering the Code

Much of this first pass is a bit kludgy. To really fit what I need, I am going to have to rebuild the bits completely, but here is a first pass.

First, I have to trick the BeginRenderTag to make sure it outputs the class. I do this by altering it to this:

protected override void RenderBeginTag(HtmlTextWriter writer)
{
    if (Extender.AdapterEnabled)
    {
        //Extender.RenderBeginTag(writer, "AspNet-Menu-" + Control.Orientation.ToString());
        Extender.RenderBeginTag(writer, "l");

    }
    else
    {
        base.RenderBeginTag(writer);
    }
}

The reason this is a bit kludgy, is I have to end this tag later. I also have to comment out line 113 (renumbered after change) to read:

//Extender.RenderEndTag(writer);

The second part of the kludge is having to alter the BuildItems routine. All of the changes are in bold.

private void BuildItems(MenuItemCollection items, bool isRoot, HtmlTextWriter writer, bool buildUl)
{
    if (items.Count > 0)
    {
        writer.WriteLine();

        if (isRoot)
        {
            writer.WriteEndTag("div");     //This is the kludge
            writer.WriteBeginTag("div");
            writer.WriteAttribute("class", "r");
            writer.Write(HtmlTextWriter.TagRightChar);
            writer.WriteEndTag("div");

        }

        if (buildUl)
        {

            writer.WriteBeginTag("ul");

            if (isRoot)
            {
                writer.WriteAttribute("class", "art-menu");
            }
            writer.Write(HtmlTextWriter.TagRightChar);
            writer.Indent++;
        }
        foreach (MenuItem item in items)
        {
            BuildItem(item, writer);
        }

        writer.Indent–;
        writer.WriteLine();
        if(buildUl)
            writer.WriteEndTag("ul");
    }
}

This means the code is broken until you do a search and replace on items that call BuildItems() – in BuildItem and RenderContents:

BuildItems(item.ChildItems, false, writer, true);

I know have some serious whacking in BuildItem. First comment out the line adding a class to the list item (lines 183-189 in my modified code):

//string theClass = (item.ChildItems.Count > 0) ? "AspNet-Menu-WithChildren" : "AspNet-Menu-Leaf";
//string selectedStatusClass = GetSelectStatusClass(item);
//if (!String.IsNullOrEmpty(selectedStatusClass))
//{
//    theClass += " " + selectedStatusClass;
//}
//writer.WriteAttribute("class", theClass);

And the class addition for the a href (lines 234-235):

//writer.WriteAttribute("class", GetItemClass(menu, item));
//WebControlAdapterExtender.WriteTargetAttribute(writer, item.Target);

Below these lines, I have to add a class for the active element:

if (item.Selected)
{
    writer.WriteAttribute("class", "active");
}

Then write out the spans for the individual items (below writer.Write(HtmlTextWriter.TagRightChar); in the alter code – line 251):

if (item.Depth < menu.StaticDisplayLevels)
{
    writer.WriteBeginTag("span");
    writer.WriteAttribute("class", "l");
    writer.Write(HtmlTextWriter.TagRightChar);
    writer.WriteEndTag("span");

    writer.WriteBeginTag("span");
    writer.WriteAttribute("class", "r");
    writer.Write(HtmlTextWriter.TagRightChar);
    writer.WriteEndTag("span");

    writer.WriteBeginTag("span");
    writer.WriteAttribute("class", "t");
    writer.Write(HtmlTextWriter.TagRightChar);
}

In the else condition, when it is not a link (line 256 in my code), comment this out:

writer.WriteBeginTag("span");
//writer.WriteAttribute("class", GetItemClass(menu, item));

And I also have to change what is now lines 309-312 to this:

if ((item.ChildItems != null) && (item.ChildItems.Count > 0))
{
    bool buildUl = item.Depth >= (menu.StaticDisplayLevels – 1);
    BuildItems(item.ChildItems, false, writer, buildUl);
}

The HTML is now ugly, but it works okay. Now to kill off dead code. Everything between

if (((item.Depth < menu.StaticDisplayLevels) && (menu.StaticItemTemplate != null)) ||
    ((item.Depth >= menu.StaticDisplayLevels) && (menu.DynamicItemTemplate != null)))

and else is dead, as we are NOT using item templates. I can also comment out GetItemClass(), GetSelectStatusClass(), IsChildItemSelected(), and IsParentItemSelected(). This reduces the source code by about 100 lines. There is still a bit of cleanup to do to make things look good. This mostly has to do with the indents. Rather than detail the changes, here is the completed code. The HTML is not perfect, but it is much cleaner than when I started:

using System;
using System.IO;
using System.Web;
using System.Web.Configuration;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Diagnostics;

namespace CSSFriendly
{
  public class MenuAdapter : System.Web.UI.WebControls.Adapters.MenuAdapter
  {
    private WebControlAdapterExtender _extender = null;
    private WebControlAdapterExtender Extender
    {
      get
      {
        if (((_extender == null) && (Control != null)) ||
          ((_extender != null) && (Control != _extender.AdaptedControl)))
        {
          _extender = new WebControlAdapterExtender(Control);
        }

        System.Diagnostics.Debug.Assert(_extender != null, "CSS Friendly adapters internal error", "Null extender instance");
        return _extender;
      }
    }

    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);

      if (Extender.AdapterEnabled)
      {
        RegisterScripts();
      }
    }

    private void RegisterScripts()
    {
      Extender.RegisterScripts();

      /*
       * Modified for support of compiled CSSFriendly assembly
       *
       * We will first search for embedded JavaScript files. If they are not
       * found, we default to the standard approach.
       */

      Type type = this.GetType();

      // TreeViewAdapter.js
      string resource = "CSSFriendly.JavaScript.MenuAdapter.js";
      string filePath = Page.ClientScript.GetWebResourceUrl(type, resource);
      // if filePath is empty, use the old approach
      if ( String.IsNullOrEmpty(filePath) )
      {
        string folderPath = WebConfigurationManager.AppSettings.Get("CSSFriendly-JavaScript-Path");
        if (String.IsNullOrEmpty(folderPath))
        {
          folderPath = "~/JavaScript";
        }
        filePath = folderPath.EndsWith("/") ? folderPath + "MenuAdapter.js" : folderPath + "/TreeViewAdapter.js";
      }

      if (!Page.ClientScript.IsClientScriptIncludeRegistered(type, resource))
        Page.ClientScript.RegisterClientScriptInclude(type, resource, Page.ResolveUrl(filePath));

      // Menu.css — only add if it is embedded
      resource = "CSSFriendly.CSS.Menu.css";
      filePath = Page.ClientScript.GetWebResourceUrl(type, resource);
      // if filePath is not empty, embedded CSS exists — register it
      if (!String.IsNullOrEmpty(filePath))
      {
        string cssTag = "<link href="" + Page.ResolveUrl(filePath) + "" type="text/css" rel="stylesheet"></link>";
        if (!Page.ClientScript.IsClientScriptBlockRegistered(type, resource))
          Page.ClientScript.RegisterClientScriptBlock(type, resource, cssTag, false);
      }

      // IEMenu6.css — only add if it is embedded
      resource = "CSSFriendly.CSS.BrowserSpecific.IEMenu6.css";
      filePath = Page.ClientScript.GetWebResourceUrl(type, resource);
      // if filePath is not empty, embedded CSS exists — register it
      if (!String.IsNullOrEmpty(filePath))
      {
        string cssTag = "<link href="" + Page.ResolveUrl(filePath) + "" type="text/css" rel="stylesheet"></link>";
        if (!Page.ClientScript.IsClientScriptBlockRegistered(type, resource))
          Page.ClientScript.RegisterClientScriptBlock(type, resource, cssTag, false);
      }
    }

    protected override void RenderBeginTag(HtmlTextWriter writer)
    {
      if (Extender.AdapterEnabled)
      {
        Extender.RenderBeginTag(writer, "l");
      }
      else
      {
        base.RenderBeginTag(writer);
      }
    }

        protected override void RenderEndTag(HtmlTextWriter writer)
        {
            if (!Extender.AdapterEnabled)
            {
                base.RenderEndTag(writer);
            }
            else
            {
                //FIX: 8/12/2009
                writer.WriteEndTag("div");
            }
        }

    protected override void RenderContents(HtmlTextWriter writer)
    {
      if (Extender.AdapterEnabled)
      {
        BuildItems(Control.Items, true, writer, true);
        writer.Indent–;
        writer.WriteLine();
      }
      else
      {
        base.RenderContents(writer);
      }
    }

    private void BuildItems(MenuItemCollection items, bool isRoot, HtmlTextWriter writer, bool buildUl)
    {
      if (items.Count > 0)
      {
        writer.WriteLine();

        if (isRoot)
        {
          writer.Indent–;     //Fix HTML
          writer.WriteEndTag("div");
          writer.WriteBeginTag("div");
          writer.WriteAttribute("class", "r");
          writer.Write(HtmlTextWriter.TagRightChar);
          writer.WriteEndTag("div");
          writer.WriteLine();    //Fix HTML
        }

        if (buildUl)
        {
          writer.WriteBeginTag("ul");

          if (isRoot)
          {
            writer.WriteAttribute("class", "art-menu");
          }
          writer.Write(HtmlTextWriter.TagRightChar);
          writer.Indent++;
        }
        foreach (MenuItem item in items)
        {
          BuildItem(item, writer);
        }

        writer.Indent–;

        writer.WriteLine();

        if(buildUl)
          writer.WriteEndTag("ul");
      }
    }

    private void BuildItem(MenuItem item, HtmlTextWriter writer)
    {
      Menu menu = Control as Menu;
      if ((menu != null) && (item != null) && (writer != null))
      {
        writer.WriteLine();
        writer.WriteBeginTag("li");

        writer.Write(HtmlTextWriter.TagRightChar);
        writer.Indent++;

          if (IsLink(item))
          {
            writer.WriteBeginTag("a");
            if (!String.IsNullOrEmpty(item.NavigateUrl))
            {
              writer.WriteAttribute("href", Page.Server.HtmlEncode(menu.ResolveClientUrl(item.NavigateUrl)));
            }
            else
            {
              writer.WriteAttribute("href", Page.ClientScript.GetPostBackClientHyperlink(menu, "b" +  item.ValuePath.Replace(menu.PathSeparator.ToString(), "\"), true));
            }

            if (item.Selected)
            {
              writer.WriteAttribute("class", "active");
            }

            if (!String.IsNullOrEmpty(item.ToolTip))
            {
              writer.WriteAttribute("title", item.ToolTip);
            }
            else if (!String.IsNullOrEmpty(menu.ToolTip))
            {
              writer.WriteAttribute("title", menu.ToolTip);
            }

            writer.Write(HtmlTextWriter.TagRightChar);

            if (item.Depth < menu.StaticDisplayLevels)
            {
              writer.WriteLine(); //Fix HTML
              writer.WriteBeginTag("span");
              writer.WriteAttribute("class", "l");
              writer.Write(HtmlTextWriter.TagRightChar);
              writer.WriteEndTag("span");

              writer.WriteBeginTag("span");
              writer.WriteAttribute("class", "r");
              writer.Write(HtmlTextWriter.TagRightChar);
              writer.WriteEndTag("span");

              writer.WriteBeginTag("span");
              writer.WriteAttribute("class", "t");
              writer.Write(HtmlTextWriter.TagRightChar);
            }

            writer.Indent++;
            writer.WriteLine();
          }
          else
          {
            writer.WriteBeginTag("span");
            writer.Write(HtmlTextWriter.TagRightChar);
            writer.WriteLine();
          }

          if (!String.IsNullOrEmpty(item.ImageUrl))
          {
            writer.WriteBeginTag("img");
            writer.WriteAttribute("src", menu.ResolveClientUrl(item.ImageUrl));
            writer.WriteAttribute("alt", !String.IsNullOrEmpty(item.ToolTip) ? item.ToolTip : (!String.IsNullOrEmpty(menu.ToolTip) ? menu.ToolTip : item.Text));
            writer.Write(HtmlTextWriter.SelfClosingTagEnd);
          }

          writer.Write(item.Text);

          if (IsLink(item))
          {
            writer.Indent–;
            if (item.Depth < menu.StaticDisplayLevels)
            {
              writer.WriteEndTag("span");
            }
            writer.WriteEndTag("a");
          }
          else
          {
            writer.WriteEndTag("span");
          }

        if ((item.ChildItems != null) && (item.ChildItems.Count > 0))
        {
          bool buildUl = item.Depth >= (menu.StaticDisplayLevels – 1);
          BuildItems(item.ChildItems, false, writer, buildUl);
        }

        if (item.Depth != 0)  //Fix HTML
          writer.Indent–;

        writer.WriteLine();
        writer.WriteEndTag("li");
      }
    }

    private bool IsLink(MenuItem item)
    {
      return (item != null) && item.Enabled && ((!String.IsNullOrEmpty(item.NavigateUrl)) || item.Selectable);
    }
  }
}

Hope you find this useful.

NOTE: The empty sitemap node (no URL) does not display in this current implementation.

Peace and Grace,
Greg

Twitter: @gbworld

3 Responses to CSS Friendly Adapters (Menu) for Artisteer templates

  1. Angela says:

    This is just what I’ve been looking for! However, what file are you editing? I’ve gotten the CSSFriendlyAdapters to work, but not sure where to find the file where you are doing all your editing. Thanks in advance for any help you can offer. This will help a ton! I like Artisteer quite a bit if I can just get it to work with my Asp.Net menus!

  2. Angela says:

    Never mind, I found it. I forgot to copy over MenuAdapters.cs to my AppCode folder. I did make one change that helped. The selected page wasn’t getting it’s css class set to active so I added this and it worked:if (Page.ResolveUrl(item.NavigateUrl) == HttpContext.Current.Request.Url.AbsolutePath) { writer.WriteAttribute("class", "active"); } if (item.Selected) { writer.WriteAttribute("class", "active"); }

  3. dilowa says:

    Use this code it works 100%:
    1. You need a javascript function (AS BELOW).
    2. You need to add this class=””> for each item in the menu. This is where you would have class=”active”
    3. Let me know if it works

    http://www.dilowa.co.za
    admin@dilowa.co.za
    ————————————————————–
    Javascript
    ————————————————————–

    public string GetClass(string page)
    {
    if (this.Request.Url.ToString().ToLower().Contains(page.ToLower())) return “active”;
    else return “link”;
    }

    —————————————————————
    sample menu line
    —————————————————————

    <a href="Default.aspx" class="”>
    Home
    —————————————————————

Leave a comment